﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.Design.Serialization;
using System.Reflection;

namespace System.ComponentModel.Design;

/// <summary>
///  The UndoEngine is a class that can be instantiated to support automatic undo.
///  Normally, to support undo, a developer must create individual undo units that consist of an undo action
///  and a redo action. This is fragile because each and every action the user performs must be wrapped in an undo unit.
///  Worse, if a user action is not wrapped in an undo unit its absence on the undo stack will break undo because each
///  individual unit always assumes that the previous unit's state is maintained.
///  The UndoEngine, on the other hand, listens to change events and can create undo and redo actions automatically.
///  All that is necessary to implement undo is to add these actions to an undo/redo stack and instantiate this class.
/// </summary>
public abstract partial class UndoEngine : IDisposable
{
    private IServiceProvider _provider;
    private readonly Stack<UndoUnit> _unitStack; // the stack of active (non-committed) units.
    private UndoUnit? _executingUnit; // the unit currently executing an undo.
    private readonly IDesignerHost _host;
    private readonly ComponentSerializationService _serializationService;
    private EventHandler? _undoingEvent;
    private EventHandler? _undoneEvent;
    private Dictionary<IComponent, List<ReferencingComponent>>? _refToRemovedComponent;

    /// <summary>
    ///  Creates a new UndoEngine. UndoEngine requires a service provider for access to various services.
    ///  The following services must be available or else UndoEngine will throw an exception:
    ///  IDesignerHost
    ///  IComponentChangeService
    ///  IDesignerSerializationService
    /// </summary>
    protected UndoEngine(IServiceProvider provider)
    {
        _provider = provider.OrThrowIfNull();
        _unitStack = new Stack<UndoUnit>();
        Enabled = true;

        // Validate that all required services are available. Because undo is a passive activity we must know
        // up front if it is going to work or not.
        _host = GetRequiredService<IDesignerHost>();
        ComponentChangeService = GetRequiredService<IComponentChangeService>();
        _serializationService = GetRequiredService<ComponentSerializationService>();

        // We need to listen to a slew of events to determine undo state.
        _host.TransactionOpening += OnTransactionOpening;
        _host.TransactionClosed += OnTransactionClosed;
        ComponentChangeService.ComponentAdding += OnComponentAdding;
        ComponentChangeService.ComponentChanging += OnComponentChanging;
        ComponentChangeService.ComponentRemoving += OnComponentRemoving;
        ComponentChangeService.ComponentAdded += OnComponentAdded;
        ComponentChangeService.ComponentChanged += OnComponentChanged;
        ComponentChangeService.ComponentRemoved += OnComponentRemoved;
        ComponentChangeService.ComponentRename += OnComponentRename;
    }

    /// <summary>
    ///  This property indicates if an undo is in progress.
    /// </summary>
    public bool UndoInProgress => _executingUnit is not null;

    /// <summary>
    ///  This property returns true if the Undo engine is currently enabled. When enabled, the undo engine tracks
    ///  changes made to the designer. When disabled, changes are ignored.
    ///  If the UndoEngine is set to disabled while in the middle of processing change notifications from the designer,
    ///  the undo engine will only ignore additional changes. That is, it will finish recording the changes that
    ///  are in process and only ignore additional changes. Caution should be used when disabling undo.
    ///  If undo is disabled it is easy to make a change that would cause other undo actions to become invalid.
    ///  For example, if myButton.Text was changed, and then myButton was renamed while undo was disabled, attempting
    ///  to undo the text change would fail because there is no longer a control called myButton.
    ///  Generally, you should never make changes to components with undo disabled unless you are certain
    ///  to put the components back the way they were before undo was disabled. An example of this would be to
    ///  replace one instance of "Button" with another, say "SuperButton", fixing up all the property values as you go.
    ///  The result is a new component, but because it has the same component name and property values,
    ///  undo state will still be consistent.
    /// </summary>
    public bool Enabled { get; set; }

    /// <summary>
    ///  This event is raised immediately before an undo action is performed.
    /// </summary>
    public event EventHandler? Undoing
    {
        add => _undoingEvent += value;
        remove => _undoingEvent -= value;
    }

    /// <summary>
    ///  This event is raised immediately after an undo action is performed. It will always be raised even if an exception is thrown.
    /// </summary>
    public event EventHandler? Undone
    {
        add => _undoneEvent += value;
        remove => _undoneEvent -= value;
    }

    /// <summary>
    ///  Adds the given undo unit into the undo stack. UndoEngine does not maintain its own undo stack,
    ///  so you must implement this method yourself.
    /// </summary>
    protected abstract void AddUndoUnit(UndoUnit unit);

    /// <summary>
    ///  This method will check to see if the current undo unit needs to be popped from the stack. If it does,
    ///  it will pop it and add it to the undo stack.
    ///  There must be at least one unit on the stack to call this method.
    ///
    ///  When calling CheckPopUnit you must supply a reason for the call.
    ///  There are three reasons:
    ///
    ///  Normal
    ///  Call with Normal if you are not calling in response to a closing transaction. For normal pop reasons,
    ///  the unit will be popped if there is no current transaction. If the unit is not empty it will be added
    ///  to the undo engine. If there is a transaction in progress, this method will do nothing.
    ///
    ///  TransactionCommit
    ///  Call with TransactionCommit if you are calling in response to a transaction closing,
    ///  and if that transaction is marked as being committed. CheckPopUnit will pop the unit off of the stack
    ///  and add it to the undo engine if it is not empty.
    ///
    ///  TransactionCancel
    ///  Call with TransactionCancel if you are calling in response to a transaction closing,
    ///  and if that transaction is marked as being cancelled. CheckPopUnit will pop the unit off of the stack.
    ///  If the unit is not empty Undo will be called on the unit to roll back the transaction work.
    ///  The unit will never be added to the undo engine.
    /// </summary>
    private void CheckPopUnit(PopUnitReason reason)
    {
        // The logic in here is subtle. This code handles both committing and cancelling of nested transactions.
        // Here's a summary of how it works:
        // 1. Each time a transaction is opened, a new unit is pushed onto the unit stack.
        // 2. When a change occurs, the change event checks to see if there is a currently executing unit.
        //        It also checks to see if the current unit stack is empty. If there is no executing unit
        //        (meaning that nothing is performing an undo right now), and if the unit stack is empty,
        //        the change event will create a new undo unit and push it on the stack.
        // 3. The change event always runs through all undo units in the undo stack and calls the corresponding
        //        change method. In the normal case of a single transaction or no transaction, this will operate on just one unit.
        // In the case of nested transactions there are two possibilities:
        //        a)  We are adding undo information to a nested transaction. We want to add the undo information
        //            to all levels of nested transactions. Why?  Because as a nested transaction is closed,
        //            it is either committed or cancelled. If committed, and if the transaction is not the top-most
        //            transaction, the transaction is actually just thrown away because its data is redundantly stored
        //            in the next transaction on the stack.
        //        b)  We are adding undo information to a nested transaction, but that undo information is being
        //            created because an undo unit is being "undone". Remember that for nested transactions each
        //            undo unit higher on the stack has all the data that the lower units have. When a lower unit
        //            is undone, it is popped from the stack and all of the changes it makes are recorded on the
        //            higher level units. This combines the "do" and "undo" data into the higher level unit, which
        //            in effect subtracts the undone data from the higher level unit.
        // 4. When a unit is undone it stores itself in a member variable called _executingUnit.
        //        All change events examine this variable and if it is set they do not create a new unit in response
        //        to a change. Instead, they just run through all the existing units. This builds the undo history
        //        for a transaction that is being rolled back.
        if (reason != PopUnitReason.Normal || !_host.InTransaction)
        {
            UndoUnit unit = _unitStack.Pop();

            if (!unit.IsEmpty)
            {
                unit.Close();

                if (reason == PopUnitReason.TransactionCancel)
                {
                    unit.Undo();
                    if (_unitStack.Count == 0)
                    {
                        DiscardUndoUnit(unit);
                    }
                }
                else
                {
                    if (_unitStack.Count == 0)
                    {
                        AddUndoUnit(unit);
                    }
                }
            }
            else
            {
                if (_unitStack.Count == 0)
                {
                    DiscardUndoUnit(unit);
                }
            }
        }
    }

    /// <summary>
    ///  This virtual method creates a new instance of an UndoUnit class. The default implementation just returns a
    ///  new instance of UndoUnit. Those providing their own UndoEngine can derive from UndoUnit to customize
    ///  the actions it performs. This is also a handy way to connect UndoEngine into an existing undo stack.
    ///  If the primary parameter is set to true, the undo unit will eventually be passed to either the AddUndoUnit
    ///  or DiscardUndoUnit methods. If the primary parameter is false, the undo unit is part of a nested transaction
    ///  and will never be passed to AddUndoUnit or DiscardUndoUnit; only the encompassing unit will be passed,
    ///  because the undo engine will either include or exclude the contents of the nested unit when it is closed.
    /// </summary>
    protected virtual UndoUnit CreateUndoUnit(string? name, bool primary)
    {
        return new UndoUnit(this, name);
    }

    internal IComponentChangeService ComponentChangeService { get; }

    /// <summary>
    ///  This method is called instead of AddUndoUnit for undo units that have been canceled.
    ///  For undo systems that just treat undo as a simple stack of undo units,
    ///  typically you do not need to override this method.
    ///  This method does give you a chance to perform any clean-up for a unit.
    /// </summary>
    protected virtual void DiscardUndoUnit(UndoUnit unit)
    {
    }

    /// <summary>
    ///  Public dispose method.
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
    }

    /// <summary>
    ///  Protected dispose implementation.
    /// </summary>
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _host.TransactionOpening -= OnTransactionOpening;
            _host.TransactionClosed -= OnTransactionClosed;

            ComponentChangeService.ComponentAdding -= OnComponentAdding;
            ComponentChangeService.ComponentChanging -= OnComponentChanging;
            ComponentChangeService.ComponentRemoving -= OnComponentRemoving;
            ComponentChangeService.ComponentAdded -= OnComponentAdded;
            ComponentChangeService.ComponentChanged -= OnComponentChanged;
            ComponentChangeService.ComponentRemoved -= OnComponentRemoved;
            ComponentChangeService.ComponentRename -= OnComponentRename;

            _provider = null!;
        }
    }

    /// <summary>
    ///  Helper function to retrieve the name of an object.
    /// </summary>
    internal string? GetName(object? obj, bool generateNew)
    {
        string? componentName = null;
        if (obj is not null)
        {
            if (TryGetService(out IReferenceService? rs))
            {
                componentName = rs.GetName(obj);
            }
            else
            {
                if (obj is IComponent comp)
                {
                    ISite? site = comp.Site;
                    if (site is not null)
                    {
                        componentName = site.Name;
                    }
                }
            }
        }

        if (componentName is null && generateNew)
        {
            componentName = obj is null ? "(null)" : obj.GetType().Name;
        }

        return componentName;
    }

    /// <summary>
    ///  Similar to GetService, but this will throw a NotSupportedException if the service is not present.
    /// </summary>
    protected object GetRequiredService(Type serviceType)
    {
        object? service = GetService(serviceType);
        if (service is null)
        {
            Exception ex = new InvalidOperationException(string.Format(SR.UndoEngineMissingService, serviceType.Name))
            {
                HelpLink = SR.UndoEngineMissingService
            };
            throw ex;
        }

        return service;
    }

    private protected T GetRequiredService<T>() => (T)GetRequiredService(typeof(T));

    /// <summary>
    ///  This just calls through to the service provider passed into the constructor.
    /// </summary>
    protected object? GetService(Type serviceType)
    {
        ArgumentNullException.ThrowIfNull(serviceType);

        return _provider?.GetService(serviceType);
    }

    private protected bool TryGetService<T>([NotNullWhen(true)] out T? service) where T : class
    {
        service = GetService(typeof(T)) as T;
        return service is not null;
    }

    private void OnComponentAdded(object? sender, ComponentEventArgs e)
    {
        foreach (UndoUnit unit in _unitStack)
        {
            unit.ComponentAdded(e);
        }

        if (_unitStack.Count > 0)
        {
            CheckPopUnit(PopUnitReason.Normal);
        }
    }

    private void OnComponentAdding(object? sender, ComponentEventArgs e)
    {
        // Open a new unit unless there is already one open or we are currently executing a unit.
        // If we need to create a unit, we will have to fabricate a good name.
        if (Enabled && _executingUnit is null && _unitStack.Count == 0)
        {
            string name;
            if (e.Component is not null)
            {
                name = string.Format(SR.UndoEngineComponentAdd1, GetName(e.Component, true));
            }
            else
            {
                name = SR.UndoEngineComponentAdd0;
            }

            _unitStack.Push(CreateUndoUnit(name, true));
        }

        // Now walk all the units and notify them. We don't care which order the units are notified.
        foreach (UndoUnit unit in _unitStack)
        {
            unit.ComponentAdding(e);
        }
    }

    private void OnComponentChanged(object? sender, ComponentChangedEventArgs e)
    {
        foreach (UndoUnit unit in _unitStack)
        {
            unit.ComponentChanged(e);
        }

        if (_unitStack.Count > 0)
        {
            CheckPopUnit(PopUnitReason.Normal);
        }
    }

    private void OnComponentChanging(object? sender, ComponentChangingEventArgs e)
    {
        // Open a new unit unless there is already one open or we are currently executing a unit.
        // If we need to create a unit, we will have to fabricate a good name.
        if (Enabled && _executingUnit is null && _unitStack.Count == 0)
        {
            string name;

            if (e.Component is null)
            {
                name = SR.UndoEngineComponentChange0;
            }
            else if (e.Member is null)
            {
                name = string.Format(SR.UndoEngineComponentChange1, GetName(e.Component, true));
            }
            else
            {
                name = string.Format(SR.UndoEngineComponentChange2, GetName(e.Component, true), e.Member.Name);
            }

            _unitStack.Push(CreateUndoUnit(name, true));
        }

        // Now walk all the units and notify them. We don't care which order the units are notified.
        foreach (UndoUnit unit in _unitStack)
        {
            unit.ComponentChanging(e);
        }
    }

    private void OnComponentRemoved(object? sender, ComponentEventArgs e)
    {
        foreach (UndoUnit unit in _unitStack)
        {
            unit.ComponentRemoved(e);
        }

        if (_unitStack.Count > 0)
        {
            CheckPopUnit(PopUnitReason.Normal);
        }

        // Now we need to raise ComponentChanged events for every component that had a reference to this removed component
        if (_refToRemovedComponent is not null && _refToRemovedComponent.Remove(e.Component!, out List<ReferencingComponent>? propsToUpdate))
        {
            foreach (ReferencingComponent ro in propsToUpdate)
            {
                ComponentChangeService.OnComponentChanged(ro.component, ro.member);
            }
        }
    }

    private void OnComponentRemoving(object? sender, ComponentEventArgs e)
    {
        // Open a new unit unless there is already one open or we are currently executing a unit.
        // If we need to create a unit, we will have to fabricate a good name.
        if (Enabled && _executingUnit is null && _unitStack.Count == 0)
        {
            string name;
            if (e.Component is not null)
            {
                name = string.Format(SR.UndoEngineComponentRemove1, GetName(e.Component, true));
            }
            else
            {
                name = SR.UndoEngineComponentRemove0;
            }

            _unitStack.Push(CreateUndoUnit(name, true));
        }

        // We need to keep track of all references in the container to the deleted component so that those references
        // can be fixed up if an undo of this "remove" occurs.
        if (Enabled && _host is not null && _host.Container is not null && ComponentChangeService is not null)
        {
            List<ReferencingComponent>? propsToUpdate = null;
            foreach (IComponent comp in _host.Container.Components)
            {
                if (comp == e.Component)
                {
                    continue;
                }

                PropertyDescriptorCollection props = TypeDescriptor.GetProperties(comp);
                foreach (PropertyDescriptor prop in props)
                {
                    if (prop.PropertyType.IsInstanceOfType(e.Component) &&
                        !prop.Attributes.Contains(DesignerSerializationVisibilityAttribute.Hidden) &&
                        !prop.IsReadOnly)
                    {
                        object? obj = null;
                        try
                        {
                            obj = prop.GetValue(comp);
                        }
                        catch (TargetInvocationException)
                        {
                            continue;
                        }

                        if (obj is not null && ReferenceEquals(obj, e.Component))
                        {
                            if (propsToUpdate is null)
                            {
                                propsToUpdate = [];

                                _refToRemovedComponent ??= [];

                                _refToRemovedComponent[e.Component] = propsToUpdate;
                            }

                            ComponentChangeService.OnComponentChanging(comp, prop);
                            propsToUpdate.Add(new ReferencingComponent(comp, prop));
                        }
                    }
                }
            }
        }

        // Now walk all the units and notify them. We don't care which order the units are notified.
        // By notifying all transactions we automatically support the cancelling of nested transactions.
        foreach (UndoUnit unit in _unitStack)
        {
            unit.ComponentRemoving(e);
        }
    }

    private void OnComponentRename(object? sender, ComponentRenameEventArgs e)
    {
        // Open a new unit unless there is already one open or we are currently executing a unit.
        // If we need to create a unit, we will have to fabricate a good name.
        if (Enabled && _executingUnit is null && _unitStack.Count == 0)
        {
            string name = string.Format(SR.UndoEngineComponentRename, e.OldName, e.NewName);
            _unitStack.Push(CreateUndoUnit(name, true));
        }

        // Now walk all the units and notify them. We don't care which order the units are notified.
        // By notifying all transactions we automatically support the cancelling of nested transactions.
        foreach (UndoUnit unit in _unitStack)
        {
            unit.ComponentRename(e);
        }
    }

    private void OnTransactionClosed(object? sender, DesignerTransactionCloseEventArgs e)
    {
        if (_executingUnit is null && _unitStack.Count > 0)
        {
            PopUnitReason reason = e.TransactionCommitted ? PopUnitReason.TransactionCommit : PopUnitReason.TransactionCancel;
            CheckPopUnit(reason);
        }
    }

    private void OnTransactionOpening(object? sender, EventArgs e)
    {
        // When a transaction is opened, we always push a new unit unless we're executing a unit.
        // We can push multiple units onto the stack to handle nested transactions.
        if (Enabled && _executingUnit is null)
        {
            _unitStack.Push(CreateUndoUnit(_host.TransactionDescription, _unitStack.Count == 0));
        }
    }

    /// <summary>
    ///  This event is raised immediately before an undo action is performed.
    /// </summary>
    protected virtual void OnUndoing(EventArgs e)
    {
        _undoingEvent?.Invoke(this, e);
    }

    /// <summary>
    ///  This event is raised immediately after an undo action is performed. It will always be raised even if an exception is thrown.
    /// </summary>
    protected virtual void OnUndone(EventArgs e)
    {
        _undoneEvent?.Invoke(this, e);
    }

    private enum PopUnitReason
    {
        Normal, TransactionCommit, TransactionCancel,
    }

    private readonly struct ReferencingComponent
    {
        public readonly IComponent component;
        public readonly MemberDescriptor member;

        public ReferencingComponent(IComponent component, MemberDescriptor member)
        {
            this.component = component;
            this.member = member;
        }
    }
}
